/**
 *  Copyright (c) 1997-2013, tinygroup.org (luo_guo@live.cn).
 *
 *  Licensed under the GPL, Version 3.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *       http://www.gnu.org/licenses/gpl.html
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 * --------------------------------------------------------------------------
 *  版权 (c) 1997-2013, tinygroup.org (luo_guo@live.cn).
 *
 *  本开源软件遵循 GPL 3.0 协议;
 *  如果您不遵循此协议，则不被允许使用此文件。
 *  你可以从下面的地址获取完整的协议文本
 *
 *       http://www.gnu.org/licenses/gpl.html
 */
package org.tinygroup.vfs.util;

import org.tinygroup.exception.TinySysRuntimeException;
import org.tinygroup.vfs.impl.ftp.HostFileNameParser;

/**
 * Utilities for dealing with URIs. See RFC 2396 for details.
 * 
 * @author <a href="http://commons.apache.org/vfs/team-list.html">Commons VFS
 *         team</a> 2005) $
 */
public final class UriParser {
	/**
	 * The set of valid separators. These are all converted to the normalised
	 * one. Does <i>not</i> contain the normalised separator
	 */
	// public static final char[] separators = {'\\'};
	public static final char TRANS_SEPARATOR = '\\';

	/**
	 * The normalised separator to use.
	 */
	private static final char SEPARATOR_CHAR = '/';

	private static final int HEX_BASE = 16;

	private static final int BITS_IN_HALF_BYTE = 4;

	private static final char LOW_MASK = 0x0F;

	private UriParser() {
	}

	/**
	 * Extracts the first element of a path.
	 * 
	 * @param name
	 *            StringBuilder containing the path.
	 * @return The first element of the path.
	 */
	public static String extractFirstElement(final StringBuilder name) {
		final int len = name.length();
		if (len < 1) {
			return null;
		}
		int startPos = 0;
		if (name.charAt(0) == SEPARATOR_CHAR) {
			startPos = 1;
		}
		for (int pos = startPos; pos < len; pos++) {
			if (name.charAt(pos) == SEPARATOR_CHAR) {
				// Found a separator
				final String elem = name.substring(startPos, pos);
				name.delete(startPos, pos + 1);
				return elem;
			}
		}

		// No separator
		final String elem = name.substring(startPos);
		name.setLength(0);
		return elem;
	}

	/**
	 * Normalises the separators in a name.
	 * 
	 * @param name
	 *            The StringBuilder containing the name
	 * @return true if the StringBuilder was modified.
	 */
	public static boolean fixSeparators(final StringBuilder name) {
		boolean changed = false;
		final int maxlen = name.length();
		for (int i = 0; i < maxlen; i++) {
			final char ch = name.charAt(i);
			if (ch == TRANS_SEPARATOR) {
				name.setCharAt(i, SEPARATOR_CHAR);
				changed = true;
			}
		}
		return changed;
	}

	/**
	 * Extracts the scheme from a URI.
	 * 
	 * @param uri
	 *            The URI.
	 * @return The scheme name. Returns null if there is no scheme.
	 */
	public static String extractScheme(final String uri) {
		return extractScheme(uri, null);
	}

	/**
	 * Extracts the scheme from a URI. Removes the scheme and ':' delimiter from
	 * the front of the URI.
	 * 
	 * @param uri
	 *            The URI.
	 * @param buffer
	 *            Returns the remainder of the URI.
	 * @return The scheme name. Returns null if there is no scheme.
	 */
	public static String extractScheme(final String uri,
			final StringBuilder buffer) {
		if (buffer != null) {
			buffer.setLength(0);
			buffer.append(uri);
		}

		final int maxPos = uri.length();
		for (int pos = 0; pos < maxPos; pos++) {
			final char ch = uri.charAt(pos);

			if (ch == ':') {
				// Found the end of the scheme
				final String scheme = uri.substring(0, pos);
				if (scheme.length() <= 1 && Os.isFamily(Os.OS_FAMILY_WINDOWS)) {
					// This is not a scheme, but a Windows drive letter
					return null;
				}
				if (buffer != null) {
					buffer.delete(0, pos + 1);
				}
				return scheme.intern();
			}

			if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
				// A scheme character
				continue;
			}
			if (pos > 0
					&& ((ch >= '0' && ch <= '9') || ch == '+' || ch == '-' || ch == '.')) {
				// A scheme character (these are not allowed as the first
				// character of the scheme, but can be used as subsequent
				// characters.
				continue;
			}

			// Not a scheme character
			break;
		}

		// No scheme in URI
		return null;
	}

	/**
	 * Removes %nn encodings from a string.
	 * 
	 * @param encodedStr
	 *            The encoded String.
	 * @return The decoded String.
	 * @throws FileSystemException
	 *             if an error occurs.
	 */
	public static String decode(final String encodedStr) {
		if (encodedStr == null) {
			return null;
		}
		if (encodedStr.indexOf('%') < 0) {
			return encodedStr;
		}
		final StringBuilder buffer = new StringBuilder(encodedStr);
		decode(buffer, 0, buffer.length());
		return buffer.toString();
	}

	/**
	 * Removes %nn encodings from a string.
	 * 
	 * @param buffer
	 *            StringBuilder containing the string to decode.
	 * @param offset
	 *            The position in the string to start decoding.
	 * @param length
	 *            The number of characters to decode.
	 * @throws FileSystemException
	 *             if an error occurs.
	 */
	public static void decode(final StringBuilder buffer, final int offset,
			final int length) {
		int index = offset;
		int count = length;
		for (; count > 0; count--, index++) {
			final char ch = buffer.charAt(index);
			if (ch != '%') {
				continue;
			}
			if (count < 3) {
				throw new TinySysRuntimeException(
						"vfs.provider/invalid-escape-sequence.error",
						buffer.substring(index, index + count));
			}

			// Decode
			int dig1 = Character.digit(buffer.charAt(index + 1), HEX_BASE);
			int dig2 = Character.digit(buffer.charAt(index + 2), HEX_BASE);
			if (dig1 == -1 || dig2 == -1) {
				throw new TinySysRuntimeException(
						"vfs.provider/invalid-escape-sequence.error",
						buffer.substring(index, index + 3));
			}
			char value = (char) (dig1 << BITS_IN_HALF_BYTE | dig2);

			// Replace
			buffer.setCharAt(index, value);
			buffer.delete(index + 1, index + 3);
			count -= 2;
		}
	}

	/**
	 * Encodes and appends a string to a StringBuilder.
	 * 
	 * @param buffer
	 *            The StringBuilder to append to.
	 * @param unencodedValue
	 *            The String to encode and append.
	 * @param reserved
	 *            characters to encode.
	 */
	public static void appendEncoded(final StringBuilder buffer,
			final String unencodedValue, final char[] reserved) {
		final int offset = buffer.length();
		buffer.append(unencodedValue);
		encode(buffer, offset, unencodedValue.length(), reserved);
	}

	/**
	 * Encodes a set of reserved characters in a StringBuilder, using the URI
	 * %nn encoding. Always encodes % characters.
	 * 
	 * @param buffer
	 *            The StringBuilder to append to.
	 * @param offset
	 *            The position in the buffer to start encoding at.
	 * @param length
	 *            The number of characters to encode.
	 * @param reserved
	 *            characters to encode.
	 */
	public static void encode(final StringBuilder buffer, final int offset,
			final int length, final char[] reserved) {
		int index = offset;
		int count = length;
		for (; count > 0; index++, count--) {
			final char ch = buffer.charAt(index);
			boolean match = ch == '%';
			if (reserved != null) {
				for (int i = 0; !match && i < reserved.length; i++) {
					if (ch == reserved[i]) {
						match = true;
					}
				}
			}
			if (match) {
				// Encode
				char[] digits = {
						Character.forDigit(
								((ch >> BITS_IN_HALF_BYTE) & LOW_MASK),
								HEX_BASE),
						Character.forDigit((ch & LOW_MASK), HEX_BASE) };
				buffer.setCharAt(index, '%');
				buffer.insert(index + 1, digits);
				index += 2;
			}
		}
	}

	/**
	 * Removes %nn encodings from a string.
	 * 
	 * @param decodedStr
	 *            The decoded String.
	 * @return The encoded String.
	 */
	public static String encode(final String decodedStr) {
		return encode(decodedStr, null);
	}

	/**
	 * Converts "special" characters to their %nn value.
	 * 
	 * @param decodedStr
	 *            The decoded String.
	 * @param reserved
	 *            Characters to encode.
	 * @return The encoded String
	 */
	public static String encode(final String decodedStr, final char[] reserved) {
		if (decodedStr == null) {
			return null;
		}
		final StringBuilder buffer = new StringBuilder(decodedStr);
		encode(buffer, 0, buffer.length(), reserved);
		return buffer.toString();
	}

	/**
	 * Encode an array of Strings.
	 * 
	 * @param strings
	 *            The array of Strings to encode.
	 * @return An array of encoded Strings.
	 */
	public static String[] encode(String[] strings) {
		if (strings == null) {
			return null;
		}
		for (int i = 0; i < strings.length; i++) {
			strings[i] = encode(strings[i]);
		}
		return strings;
	}

	/**
	 * Decodes the String.
	 * 
	 * @param uri
	 *            The String to decode.
	 * @throws FileSystemException
	 *             if an error occurs.
	 */
	public static void checkUriEncoding(String uri) {
		decode(uri);
	}

	public static void canonicalizePath(StringBuilder buffer, int offset,
			int length, HostFileNameParser fileNameParser) {
		int index = offset;
		int count = length;
		for (; count > 0; count--, index++) {
			final char ch = buffer.charAt(index);
			if (ch == '%') {
				if (count < 3) {
					throw new TinySysRuntimeException(
							"vfs.provider/invalid-escape-sequence.error",
							buffer.substring(index, index + count));
				}

				// Decode
				int dig1 = Character.digit(buffer.charAt(index + 1), HEX_BASE);
				int dig2 = Character.digit(buffer.charAt(index + 2), HEX_BASE);
				if (dig1 == -1 || dig2 == -1) {
					throw new TinySysRuntimeException(
							"vfs.provider/invalid-escape-sequence.error",
							buffer.substring(index, index + 3));
				}
				char value = (char) (dig1 << BITS_IN_HALF_BYTE | dig2);

				boolean match = value == '%'
						|| (fileNameParser != null && fileNameParser
								.encodeCharacter(value));

				if (match) {
					// this is a reserved character, not allowed to decode
					index += 2;
					count -= 2;
					continue;
				}

				// Replace
				buffer.setCharAt(index, value);
				buffer.delete(index + 1, index + 3);
				count -= 2;
			} else if (fileNameParser.encodeCharacter(ch)) {
				// Encode
				char[] digits = {
						Character.forDigit(
								((ch >> BITS_IN_HALF_BYTE) & LOW_MASK),
								HEX_BASE),
						Character.forDigit((ch & LOW_MASK), HEX_BASE) };
				buffer.setCharAt(index, '%');
				buffer.insert(index + 1, digits);
				index += 2;
			}
		}
	}

	/**
	 * Extract the query String from the URI.
	 * 
	 * @param name
	 *            StringBuilder containing the URI.
	 * @return The query string, if any. null otherwise.
	 */
	public static String extractQueryString(StringBuilder name) {
		for (int pos = 0; pos < name.length(); pos++) {
			if (name.charAt(pos) == '?') {
				String queryString = name.substring(pos + 1);
				name.delete(pos, name.length());
				return queryString;
			}
		}

		return null;
	}

}
